@gpc-cli/core 0.10.3 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,4 +79,4 @@ const analysis = await analyzeBundle("./app.aab");
79
79
 
80
80
  ## Licensing
81
81
 
82
- Free to use. Source code is on GitHub at [yasserstudio/gpc](https://github.com/yasserstudio/gpc).
82
+ MIT-licensed open source and free to use. Source code is on GitHub at [yasserstudio/gpc](https://github.com/yasserstudio/gpc).
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { OutputFormat, ResolvedConfig, WebhookConfig } from '@gpc-cli/config';
2
2
  import { AuthClient } from '@gpc-cli/auth';
3
3
  import { GpcPlugin, PluginManifest, CommandEvent, CommandResult, PluginError, RequestEvent, ResponseEvent, PluginCommand } from '@gpc-cli/plugin-sdk';
4
- import { PlayApiClient, EditCommitOptions, Track, ExternallyHostedApk, ExternallyHostedApkResponse, DeobfuscationFileType, UploadProgressEvent, ResumableUploadOptions, Listing, ImageType, CountryAvailability, Image, AppDetails, Review, ReviewReplyResponse, Subscription, SubscriptionOffer, OffersListResponse, BasePlanMigratePricesRequest, InAppProduct, ConsumptionUsageEvent, RefundPreference, Order, SubscriptionDeferResponse, SubscriptionsV2DeferResponse, ProductPurchase, ProductPurchaseV2, SubscriptionPurchaseV2, VoidedPurchase, UsersApiClient, User, DeveloperLevelPermission, Grant, MetricRow, ReportingDimension, ReportingAggregation, VitalsMetricSet, ReportingApiClient, AnomalyDetectionResponse, MetricSetResponse, ErrorIssuesResponse, ConvertRegionPricesResponse, ReportType, StatsDimension, Testers, AppRecoveryTargeting, AppRecoveryAction, CreateAppRecoveryActionRequest, DataSafetyResponse, DataSafetyRequest, ExternalTransaction, ExternalTransactionRefund, DeviceTierConfig, OneTimeOffer, OneTimeProduct, OneTimeOffersListResponse, OneTimeProductsListResponse, GamesConfigClient, AchievementConfiguration, LeaderboardConfiguration, AchievementConfigurationListResponse, GamesApiClient, Achievement, LeaderboardConfigurationListResponse, Leaderboard, ImageConfiguration, EnterpriseApiClient, CustomApp, GeneratedApksPerSigningKey, Bundle } from '@gpc-cli/api';
4
+ import { PlayApiClient, EditCommitOptions, Track, ExternallyHostedApk, ExternallyHostedApkResponse, DeobfuscationFileType, UploadProgressEvent, ResumableUploadOptions, Listing, ImageType, CountryAvailability, Image, AppDetails, Review, ReviewReplyResponse, Subscription, SubscriptionOffer, OffersListResponse, BasePlanMigratePricesRequest, InAppProduct, ConsumptionUsageEvent, RefundPreference, Order, SubscriptionDeferResponse, SubscriptionsV2DeferResponse, ProductPurchase, ProductPurchaseV2, SubscriptionPurchaseV2, VoidedPurchase, UsersApiClient, User, DeveloperLevelPermission, Grant, MetricRow, ReportingDimension, ReportingAggregation, VitalsMetricSet, ReportingApiClient, AnomalyDetectionResponse, MetricSetResponse, ErrorIssuesResponse, ConvertRegionPricesResponse, ReportType, StatsDimension, Testers, AppRecoveryTargeting, AppRecoveryAction, CreateAppRecoveryActionRequest, DataSafetyResponse, DataSafetyRequest, ExternalTransactionRefund, ExternalTransaction, DeviceTierConfig, OneTimeOffer, OneTimeProduct, OneTimeOffersListResponse, OneTimeProductsListResponse, GamesConfigClient, AchievementConfiguration, LeaderboardConfiguration, AchievementConfigurationListResponse, GamesApiClient, Achievement, LeaderboardConfigurationListResponse, Leaderboard, ImageConfiguration, EnterpriseApiClient, CustomApp, GeneratedApksPerSigningKey, Bundle } from '@gpc-cli/api';
5
5
 
6
6
  declare class GpcError extends Error {
7
7
  readonly code: string;
@@ -693,6 +693,7 @@ declare function reviewOrderRefund(client: PlayApiClient, packageName: string, o
693
693
  declare const PERMISSION_PROPAGATION_WARNING = "Note: Permission changes may take up to 48 hours to propagate.";
694
694
  interface ListUsersOptions {
695
695
  pageToken?: string;
696
+ /** @deprecated Ignored: Google does not paginate users.list. */
696
697
  pageSize?: number;
697
698
  limit?: number;
698
699
  nextPage?: string;
@@ -1034,6 +1035,21 @@ declare function importDataSafety(client: PlayApiClient, packageName: string, fi
1034
1035
  declare function createExternalTransaction(client: PlayApiClient, packageName: string, data: ExternalTransaction, externalTransactionId: string): Promise<ExternalTransaction>;
1035
1036
  declare function getExternalTransaction(client: PlayApiClient, packageName: string, transactionId: string): Promise<ExternalTransaction>;
1036
1037
  declare function refundExternalTransaction(client: PlayApiClient, packageName: string, transactionId: string, refundData: ExternalTransactionRefund): Promise<ExternalTransaction>;
1038
+ interface ExternalTransactionRefundOptions {
1039
+ full?: boolean;
1040
+ /** Pre-tax amount in micros, e.g. "1990000" for 1.99. */
1041
+ partialAmount?: string;
1042
+ currency?: string;
1043
+ refundId?: string;
1044
+ /** When the refund happened; defaults to now. */
1045
+ refundTime?: string;
1046
+ }
1047
+ /**
1048
+ * Build a refund request from CLI options. Google requires refundTime on every
1049
+ * refund and a unique refundId on each partial refund; a refund must name its
1050
+ * kind explicitly so a mistyped amount can never become a full refund.
1051
+ */
1052
+ declare function buildExternalTransactionRefund(options: ExternalTransactionRefundOptions, now?: Date): ExternalTransactionRefund;
1037
1053
 
1038
1054
  declare function listDeviceTiers(client: PlayApiClient, packageName: string): Promise<DeviceTierConfig[]>;
1039
1055
  declare function getDeviceTier(client: PlayApiClient, packageName: string, configId: string): Promise<DeviceTierConfig>;
@@ -1245,6 +1261,10 @@ interface PreflightContext {
1245
1261
  aabPath?: string;
1246
1262
  manifest?: ParsedManifest;
1247
1263
  zipEntries?: ZipEntryInfo[];
1264
+ /** True for an AAB, false for an APK (decided by the reader from the file type). */
1265
+ isAppBundle?: boolean;
1266
+ /** AAB modules that are not part of the first download (on-demand, fast-follow). */
1267
+ deferredModules?: string[];
1248
1268
  /** First 4096 bytes of each .so file, keyed by ZIP entry path. */
1249
1269
  nativeLibHeaders?: EntryHeaderMap;
1250
1270
  metadataDir?: string;
@@ -1981,4 +2001,4 @@ declare function readinessBadgeColor(grade: ReadinessGrade): string;
1981
2001
  declare function readinessBadgeUrl(score: ReadinessScore): string;
1982
2002
  declare function readinessBadgeMarkdown(score: ReadinessScore): string;
1983
2003
 
1984
- export { ApiError, type ApiSigningFingerprint, type AppInfo, type AppStatus, type ApplyReleaseNotesResult, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type BundleSizeCheckResult, type BundleSizeConfig, type BundlesWaitOptions, type ChangelogEntry, type ChecklistInput, type ChecklistItem, type ChecklistResult, type CommandContext, type CommitCluster, type CommitResult, ConfigError, type CreateEnterpriseAppParams, DEFAULT_LIMITS, DEFAULT_MODELS, DEFAULT_PREFLIGHT_CONFIG, DEFAULT_WATCH_THRESHOLDS, type DecodedNotification, type DiffToken, type DiscoverPluginsOptions, type DiscoveredPlugin, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ErrorReason, type ExportImagesOptions, type ExportImagesSummary, type FastlaneDetection, type FastlaneLane, type FetchChangelogOptions, type FieldLintResult, type FileValidationResult, type FinancialReportDownload, type FindingSeverity, GOOGLE_PLAY_LANGUAGES, type GameConfigDiff, type GamesPushOptions, type GamesSyncResult, type GenerateOptions, type GeneratedChangelog, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, type GitRunner, GpcError, type ImageSyncDetail, type ImageSyncOptions, type ImageSyncResult, type ImageValidationResult, type InitOptions, type InitResult, type InternalSharingUploadResult, type KeystoreFingerprint, type ListGamesConfigOptions, type ListIapOptions, type ListReportsResult, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingFieldLimits, type ListingLintResult, type ListingsResult, type LoadedPlugin, type LocaleBundle, type LocaleEntry, type MigrationResult, NetworkError, type OneTimeProductDiff, type OutputMode, PERMISSION_PROPAGATION_WARNING, PLACEHOLDER_TEXT, PLAY_STORE_LIMIT, PROVIDER_WHITELIST, type ParsedCommit, type ParsedManifest, type ParsedMonth, type PlayStoreFormat, type PlayStoreRenderOptions, PluginManager, type PreflightConfig, type PreflightFinding, type PreflightOptions, type PreflightResult, type PreflightScanner, type Provider, type PublishOptions, type PublishResult, type PushResult, type QuotaUsage, RENDERERS, REVIEW_SORT_PRESETS, type RawCommit, type ReadinessBreakdownEntry, type ReadinessGrade, type ReadinessScore, type ReadinessSignal, type ReadinessSignalStatus, type RefundReviewPreference, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type Renderer, type ReportObject, type ReportsAuth, type ResolveAiConfigOptions, type ResolveLocalesOptions, type ReviewAnalysis, type ReviewExportOptions, type ReviewOrderRefundOptions, type ReviewOrderRefundResult, type ReviewSortPreset, type ReviewsFilterOptions, type RevocationRefundType, type RtdnStatus, SECTION_ORDER, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, SEVERITY_ORDER, type ScaffoldOptions, type ScaffoldResult, type SigningConsistencyResult, type SigningKeyComparison, type Spinner, type StatsReportDownload, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionAnalytics, type SubscriptionDiff, type SyncResult, type ThresholdResult, type TrainConfig, type TrainState, type TranslateBundleOptions, type TranslatedBundle, type TranslationFailure, type TranslationPath, type TranslationResult, type Translator, type TranslatorConfig, type UploadResult, VALID_WATCH_METRICS, type ValidateCheck, type ValidateOptions, type ValidateResult, type ValidatedPluginApproval, type VersionVitalsComparison, type VersionVitalsRow, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchAction, type WatchCallbacks, type WatchConfig, type WatchEvent, type WatchMetric, type WatchOptions, type WatchRollout, type WatchSummary, type WatchVitalReading, type WatchVitalsOptions, type WebhookPayload, type ZipCsvEntry, abortTrain, acknowledgeProductPurchase, activateBasePlan, activateOffer, addRecoveryTargeting, addTesters, advanceTrain, analyzeBundle, analyzeRemoteListings, analyzeReviews, annotateListResult, applyReleaseNotes, assignRelease, batchGetOrders, batchSyncInAppProducts, buildChecklist, buildLocaleBundle, bundleToReleaseNotes, cancelRecoveryAction, cancelSubscriptionPurchase, cancelSubscriptionV2, checkBundleSize, checkSigningConsistency, checkThreshold, classifyError, clearAuditLog, commitWithRescue, compareBundles, compareFingerprints, compareVersionVitals, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAchievementConfig, createAuditEntry, createDeviceTier, createEnterpriseApp, createExternalTransaction, createGrant, createInAppProduct, createLeaderboardConfig, createOffer, createOneTimeOffer, createOneTimeProduct, createRecoveryAction, createSpinner, createSubscription, createTrack, createTranslator, deactivateBasePlan, deactivateOffer, decodeCsvText, decodeNotification, decodeStatsCsv, defaultGitRunner, deferSubscriptionPurchase, deferSubscriptionV2, deleteAchievementConfig, deleteBasePlan, deleteGrant, deleteImage, deleteInAppProduct, deleteLeaderboardConfig, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffAchievementConfig, diffLeaderboardConfig, diffListings, diffListingsCommand, diffListingsEnhanced, diffOneTimeProduct, diffReleases, diffSubscription, discoverPluginEntries, discoverPlugins, dispatchWebhook, downloadFinancialReport, downloadGeneratedApk, downloadReportObject, downloadStatsReport, exportImages, exportReviews, extractCsvEntriesFromZip, fetchAggregateCost, fetchChangelog, fetchReleaseNotes, findBundle, formatChangelogEntry, formatCustomPayload, formatDiscordPayload, formatJunit, formatMoney, formatNotification, formatOutput, formatPathLabel, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, formatWordDiff, generateChangelog, generateMigrationPlan, generateNotesFromGit, getAchievementConfig, getAllScannerNames, getApiSigningFingerprint, getAppInfo, getAppStatus, getCountryAvailability, getDeviceTier, getExternalTransaction, getInAppProduct, getKeystoreFingerprint, getLeaderboardConfig, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getOrderDetails, getProductPurchase, getProductPurchaseV2, getQuotaUsage, getReleasesStatus, getReview, getRtdnStatus, getSubscription, getSubscriptionAnalytics, getSubscriptionPurchase, getTrainStatus, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsErrorCount, getVitalsLmk, getVitalsMemory, getVitalsMemoryBitmap, getVitalsMemoryRss, getVitalsOverview, getVitalsRendering, getVitalsStartup, gradeFromPercent, gunzipIfNeeded, handleBreach, importDataSafety, importTestersFromCsv, initAudit, initProject, inviteUser, isFinancialReportType, isGzip, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, isVersionedNotesDir, isZip, lintListing, lintListings, lintLocalListings, listAchievementConfigs, listAchievements, listAuditEvents, listBundles, listDeviceTiers, listGeneratedApks, listGrants, listImages, listInAppProducts, listLeaderboardConfigs, listLeaderboards, listOffers, listOneTimeOffers, listOneTimeProducts, listRecoveryActions, listReportObjects, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadPreflightConfig, loadStatusCache, maybePaginate, migratePrices, monthToken, moreResultsFooter, normalizeFingerprint, parseAppfile, parseCommit, parseFastfile, parseGrantArg, parseKeytoolOutput, parseMonth, parseRemoteUrl, pauseTrain, promoteRelease, publish, publishEnterpriseApp, pullAchievementConfigs, pullLeaderboardConfigs, pullListings, pushAchievementConfigs, pushLeaderboardConfigs, pushListings, readListingsFromDir, readReleaseNotesForVersion, readReleaseNotesFromDir, readinessBadgeColor, readinessBadgeMarkdown, readinessBadgeUrl, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, relativeTime, removeTesters, removeUser, renderChecklistMarkdown, renderJson, renderMarkdown, renderPlayStore, renderPlayStoreJson, renderPlayStoreMd, renderPlayStorePrompt, renderPrompt, replyToReview, reportAccessDeniedError, reportBucketNotFoundError, reportObjectNotFoundError, reportPrefix, resolveAiConfig, resolveLocales, resolveReportsBucket, retryOnUploadNotComplete, reviewOrderRefund, revokeSubscriptionPurchase, runPreflight, runWatch, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, scoreReadiness, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, setAchievementIcon, setLeaderboardIcon, sha256File, sortResults, sortReviews, startTrain, statusHasBreach, syncImages, syncInAppProducts, topFiles, trackBreachState, translateBundle, updateAchievementConfig, updateAppDetails, updateDataSafety, updateGrant, updateInAppProduct, updateLeaderboardConfig, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateAndCommit, validateBundleForApply, validateImage, validateLanguageCode, validatePackageName, validatePluginForApproval, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, waitForBundle, waitForBundleProcessing, watchVitalsWithAutoHalt, wordDiff, writeAuditLog, writeListingsToDir, writeMigrationOutput };
2004
+ export { ApiError, type ApiSigningFingerprint, type AppInfo, type AppStatus, type ApplyReleaseNotesResult, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type BundleSizeCheckResult, type BundleSizeConfig, type BundlesWaitOptions, type ChangelogEntry, type ChecklistInput, type ChecklistItem, type ChecklistResult, type CommandContext, type CommitCluster, type CommitResult, ConfigError, type CreateEnterpriseAppParams, DEFAULT_LIMITS, DEFAULT_MODELS, DEFAULT_PREFLIGHT_CONFIG, DEFAULT_WATCH_THRESHOLDS, type DecodedNotification, type DiffToken, type DiscoverPluginsOptions, type DiscoveredPlugin, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ErrorReason, type ExportImagesOptions, type ExportImagesSummary, type ExternalTransactionRefundOptions, type FastlaneDetection, type FastlaneLane, type FetchChangelogOptions, type FieldLintResult, type FileValidationResult, type FinancialReportDownload, type FindingSeverity, GOOGLE_PLAY_LANGUAGES, type GameConfigDiff, type GamesPushOptions, type GamesSyncResult, type GenerateOptions, type GeneratedChangelog, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, type GitRunner, GpcError, type ImageSyncDetail, type ImageSyncOptions, type ImageSyncResult, type ImageValidationResult, type InitOptions, type InitResult, type InternalSharingUploadResult, type KeystoreFingerprint, type ListGamesConfigOptions, type ListIapOptions, type ListReportsResult, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingFieldLimits, type ListingLintResult, type ListingsResult, type LoadedPlugin, type LocaleBundle, type LocaleEntry, type MigrationResult, NetworkError, type OneTimeProductDiff, type OutputMode, PERMISSION_PROPAGATION_WARNING, PLACEHOLDER_TEXT, PLAY_STORE_LIMIT, PROVIDER_WHITELIST, type ParsedCommit, type ParsedManifest, type ParsedMonth, type PlayStoreFormat, type PlayStoreRenderOptions, PluginManager, type PreflightConfig, type PreflightFinding, type PreflightOptions, type PreflightResult, type PreflightScanner, type Provider, type PublishOptions, type PublishResult, type PushResult, type QuotaUsage, RENDERERS, REVIEW_SORT_PRESETS, type RawCommit, type ReadinessBreakdownEntry, type ReadinessGrade, type ReadinessScore, type ReadinessSignal, type ReadinessSignalStatus, type RefundReviewPreference, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type Renderer, type ReportObject, type ReportsAuth, type ResolveAiConfigOptions, type ResolveLocalesOptions, type ReviewAnalysis, type ReviewExportOptions, type ReviewOrderRefundOptions, type ReviewOrderRefundResult, type ReviewSortPreset, type ReviewsFilterOptions, type RevocationRefundType, type RtdnStatus, SECTION_ORDER, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, SEVERITY_ORDER, type ScaffoldOptions, type ScaffoldResult, type SigningConsistencyResult, type SigningKeyComparison, type Spinner, type StatsReportDownload, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionAnalytics, type SubscriptionDiff, type SyncResult, type ThresholdResult, type TrainConfig, type TrainState, type TranslateBundleOptions, type TranslatedBundle, type TranslationFailure, type TranslationPath, type TranslationResult, type Translator, type TranslatorConfig, type UploadResult, VALID_WATCH_METRICS, type ValidateCheck, type ValidateOptions, type ValidateResult, type ValidatedPluginApproval, type VersionVitalsComparison, type VersionVitalsRow, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchAction, type WatchCallbacks, type WatchConfig, type WatchEvent, type WatchMetric, type WatchOptions, type WatchRollout, type WatchSummary, type WatchVitalReading, type WatchVitalsOptions, type WebhookPayload, type ZipCsvEntry, abortTrain, acknowledgeProductPurchase, activateBasePlan, activateOffer, addRecoveryTargeting, addTesters, advanceTrain, analyzeBundle, analyzeRemoteListings, analyzeReviews, annotateListResult, applyReleaseNotes, assignRelease, batchGetOrders, batchSyncInAppProducts, buildChecklist, buildExternalTransactionRefund, buildLocaleBundle, bundleToReleaseNotes, cancelRecoveryAction, cancelSubscriptionPurchase, cancelSubscriptionV2, checkBundleSize, checkSigningConsistency, checkThreshold, classifyError, clearAuditLog, commitWithRescue, compareBundles, compareFingerprints, compareVersionVitals, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAchievementConfig, createAuditEntry, createDeviceTier, createEnterpriseApp, createExternalTransaction, createGrant, createInAppProduct, createLeaderboardConfig, createOffer, createOneTimeOffer, createOneTimeProduct, createRecoveryAction, createSpinner, createSubscription, createTrack, createTranslator, deactivateBasePlan, deactivateOffer, decodeCsvText, decodeNotification, decodeStatsCsv, defaultGitRunner, deferSubscriptionPurchase, deferSubscriptionV2, deleteAchievementConfig, deleteBasePlan, deleteGrant, deleteImage, deleteInAppProduct, deleteLeaderboardConfig, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffAchievementConfig, diffLeaderboardConfig, diffListings, diffListingsCommand, diffListingsEnhanced, diffOneTimeProduct, diffReleases, diffSubscription, discoverPluginEntries, discoverPlugins, dispatchWebhook, downloadFinancialReport, downloadGeneratedApk, downloadReportObject, downloadStatsReport, exportImages, exportReviews, extractCsvEntriesFromZip, fetchAggregateCost, fetchChangelog, fetchReleaseNotes, findBundle, formatChangelogEntry, formatCustomPayload, formatDiscordPayload, formatJunit, formatMoney, formatNotification, formatOutput, formatPathLabel, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, formatWordDiff, generateChangelog, generateMigrationPlan, generateNotesFromGit, getAchievementConfig, getAllScannerNames, getApiSigningFingerprint, getAppInfo, getAppStatus, getCountryAvailability, getDeviceTier, getExternalTransaction, getInAppProduct, getKeystoreFingerprint, getLeaderboardConfig, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getOrderDetails, getProductPurchase, getProductPurchaseV2, getQuotaUsage, getReleasesStatus, getReview, getRtdnStatus, getSubscription, getSubscriptionAnalytics, getSubscriptionPurchase, getTrainStatus, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsErrorCount, getVitalsLmk, getVitalsMemory, getVitalsMemoryBitmap, getVitalsMemoryRss, getVitalsOverview, getVitalsRendering, getVitalsStartup, gradeFromPercent, gunzipIfNeeded, handleBreach, importDataSafety, importTestersFromCsv, initAudit, initProject, inviteUser, isFinancialReportType, isGzip, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, isVersionedNotesDir, isZip, lintListing, lintListings, lintLocalListings, listAchievementConfigs, listAchievements, listAuditEvents, listBundles, listDeviceTiers, listGeneratedApks, listGrants, listImages, listInAppProducts, listLeaderboardConfigs, listLeaderboards, listOffers, listOneTimeOffers, listOneTimeProducts, listRecoveryActions, listReportObjects, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadPreflightConfig, loadStatusCache, maybePaginate, migratePrices, monthToken, moreResultsFooter, normalizeFingerprint, parseAppfile, parseCommit, parseFastfile, parseGrantArg, parseKeytoolOutput, parseMonth, parseRemoteUrl, pauseTrain, promoteRelease, publish, publishEnterpriseApp, pullAchievementConfigs, pullLeaderboardConfigs, pullListings, pushAchievementConfigs, pushLeaderboardConfigs, pushListings, readListingsFromDir, readReleaseNotesForVersion, readReleaseNotesFromDir, readinessBadgeColor, readinessBadgeMarkdown, readinessBadgeUrl, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, relativeTime, removeTesters, removeUser, renderChecklistMarkdown, renderJson, renderMarkdown, renderPlayStore, renderPlayStoreJson, renderPlayStoreMd, renderPlayStorePrompt, renderPrompt, replyToReview, reportAccessDeniedError, reportBucketNotFoundError, reportObjectNotFoundError, reportPrefix, resolveAiConfig, resolveLocales, resolveReportsBucket, retryOnUploadNotComplete, reviewOrderRefund, revokeSubscriptionPurchase, runPreflight, runWatch, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, scoreReadiness, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, setAchievementIcon, setLeaderboardIcon, sha256File, sortResults, sortReviews, startTrain, statusHasBreach, syncImages, syncInAppProducts, topFiles, trackBreachState, translateBundle, updateAchievementConfig, updateAppDetails, updateDataSafety, updateGrant, updateInAppProduct, updateLeaderboardConfig, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateAndCommit, validateBundleForApply, validateImage, validateLanguageCode, validatePackageName, validatePluginForApproval, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, waitForBundle, waitForBundleProcessing, watchVitalsWithAutoHalt, wordDiff, writeAuditLog, writeListingsToDir, writeMigrationOutput };
package/dist/index.js CHANGED
@@ -4551,7 +4551,7 @@ async function listUsers(client, developerId, options) {
4551
4551
  if (options?.limit || options?.nextPage) {
4552
4552
  const result = await paginateAll5(
4553
4553
  async (pageToken) => {
4554
- const resp = await client.list(developerId, { pageToken, pageSize: options?.pageSize });
4554
+ const resp = await client.list(developerId, { pageToken });
4555
4555
  return { items: resp.users || [], nextPageToken: resp.nextPageToken };
4556
4556
  },
4557
4557
  { limit: options.limit, startPageToken: options.nextPage }
@@ -4880,6 +4880,88 @@ async function getExternalTransaction(client, packageName, transactionId) {
4880
4880
  async function refundExternalTransaction(client, packageName, transactionId, refundData) {
4881
4881
  return client.externalTransactions.refund(packageName, transactionId, refundData);
4882
4882
  }
4883
+ var RFC3339_RE = /^(\d{4}-\d{2}-\d{2})T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/i;
4884
+ var MAX_INT64 = 2n ** 63n - 1n;
4885
+ var FUTURE_SKEW_MS = 5 * 60 * 1e3;
4886
+ function refundUsageError(message, suggestion) {
4887
+ return new GpcError(message, "EXT_TXN_REFUND_INVALID", 2, suggestion);
4888
+ }
4889
+ function buildExternalTransactionRefund(options, now = /* @__PURE__ */ new Date()) {
4890
+ const partialAmount = options.partialAmount?.trim();
4891
+ const currency = options.currency?.trim();
4892
+ const refundId = options.refundId?.trim();
4893
+ if (options.full && partialAmount) {
4894
+ throw refundUsageError(
4895
+ "Use --full or --partial-amount, not both",
4896
+ "Pass --full to refund the whole transaction, or --partial-amount for part of it."
4897
+ );
4898
+ }
4899
+ if (!options.full && !partialAmount) {
4900
+ throw refundUsageError(
4901
+ "Choose the refund type with --full or --partial-amount",
4902
+ "Pass --full, or --partial-amount <micros> --currency <code> --refund-id <id>."
4903
+ );
4904
+ }
4905
+ let refundTime = now.toISOString();
4906
+ if (options.refundTime !== void 0) {
4907
+ const raw = options.refundTime.trim();
4908
+ const match = RFC3339_RE.exec(raw);
4909
+ const parsed = new Date(raw);
4910
+ const offsetMs = parsed.getTime() - Date.parse(raw.replace(/(?:Z|[+-]\d{2}:\d{2})$/i, "Z"));
4911
+ const calendarDate = match ? new Date(parsed.getTime() - offsetMs).toISOString().slice(0, 10) : void 0;
4912
+ if (!match || Number.isNaN(parsed.getTime()) || calendarDate !== match[1]) {
4913
+ throw refundUsageError(
4914
+ `Invalid --refund-time "${options.refundTime}"`,
4915
+ "Use an RFC 3339 timestamp with a timezone, for example 2026-09-25T12:00:00Z or 2026-09-25T14:00:00+02:00."
4916
+ );
4917
+ }
4918
+ if (parsed.getTime() > now.getTime() + FUTURE_SKEW_MS) {
4919
+ throw refundUsageError(
4920
+ `--refund-time "${options.refundTime}" is in the future`,
4921
+ "Give the time the refund actually happened, or omit --refund-time to use now."
4922
+ );
4923
+ }
4924
+ refundTime = parsed.toISOString();
4925
+ }
4926
+ if (options.full) {
4927
+ if (currency || refundId) {
4928
+ throw refundUsageError(
4929
+ "--currency and --refund-id only apply to --partial-amount",
4930
+ "Remove them for a full refund, or use --partial-amount instead of --full."
4931
+ );
4932
+ }
4933
+ return { refundTime, fullRefund: {} };
4934
+ }
4935
+ if (!partialAmount || !/^\d{1,19}$/.test(partialAmount) || BigInt(partialAmount) <= 0n || BigInt(partialAmount) > MAX_INT64) {
4936
+ throw refundUsageError(
4937
+ `Invalid --partial-amount "${options.partialAmount}"`,
4938
+ "Give the pre-tax amount in micros as a positive whole number, for example 1990000 for 1.99."
4939
+ );
4940
+ }
4941
+ if (!currency || !/^[A-Za-z]{3}$/.test(currency)) {
4942
+ throw refundUsageError(
4943
+ currency ? `Invalid --currency "${currency}"` : "--currency is required with --partial-amount",
4944
+ "Use the transaction's ISO 4217 currency code, for example USD."
4945
+ );
4946
+ }
4947
+ if (!refundId) {
4948
+ throw refundUsageError(
4949
+ "--refund-id is required with --partial-amount",
4950
+ "Give each partial refund a unique ID; Google rejects a second refund with the same ID."
4951
+ );
4952
+ }
4953
+ return {
4954
+ refundTime,
4955
+ partialRefund: {
4956
+ refundId,
4957
+ // Normalized, so "007" is sent as "7".
4958
+ refundPreTaxAmount: {
4959
+ priceMicros: BigInt(partialAmount).toString(),
4960
+ currency: currency.toUpperCase()
4961
+ }
4962
+ }
4963
+ };
4964
+ }
4883
4965
 
4884
4966
  // src/commands/device-tiers.ts
4885
4967
  async function listDeviceTiers(client, packageName) {
@@ -5832,7 +5914,7 @@ async function initProject(options) {
5832
5914
  {
5833
5915
  failOn: "error",
5834
5916
  targetSdkMinimum: 36,
5835
- maxDownloadSizeMb: 150,
5917
+ maxDownloadSizeMb: 200,
5836
5918
  allowedPermissions: [],
5837
5919
  disabledRules: [],
5838
5920
  severityOverrides: {}
@@ -5950,7 +6032,7 @@ var SEVERITY_ORDER = {
5950
6032
  var DEFAULT_PREFLIGHT_CONFIG = {
5951
6033
  failOn: "error",
5952
6034
  targetSdkMinimum: 36,
5953
- maxDownloadSizeMb: 150,
6035
+ maxDownloadSizeMb: 200,
5954
6036
  allowedPermissions: [],
5955
6037
  disabledRules: [],
5956
6038
  severityOverrides: {}
@@ -6083,13 +6165,23 @@ function getSchema() {
6083
6165
  return cachedSchema;
6084
6166
  }
6085
6167
  function decodeManifest(buf) {
6168
+ return extractManifestData(decodeManifestElement(buf));
6169
+ }
6170
+ function isInstallTimeModule(buf) {
6171
+ const module = getChildren(decodeManifestElement(buf), "module")[0];
6172
+ if (!module) return true;
6173
+ const delivery = getChildren(module, "delivery")[0];
6174
+ if (delivery) return getChildren(delivery, "install-time").length > 0;
6175
+ return !getBoolByName(module.attribute || [], "onDemand", false);
6176
+ }
6177
+ function decodeManifestElement(buf) {
6086
6178
  const root = getSchema();
6087
6179
  const XmlNode = root.lookupType("aapt.pb.XmlNode");
6088
6180
  const decoded = XmlNode.decode(buf);
6089
6181
  if (!decoded.element || decoded.element.name !== "manifest") {
6090
6182
  throw new Error("Invalid AAB manifest: root element is not <manifest>");
6091
6183
  }
6092
- return extractManifestData(decoded.element);
6184
+ return decoded.element;
6093
6185
  }
6094
6186
  function getAttrByName(attrs, name) {
6095
6187
  const attr = attrs.find((a) => a.name === name || RESOURCE_IDS[a.resourceId] === name);
@@ -6198,6 +6290,9 @@ var AAB_MANIFEST_PATH = "base/manifest/AndroidManifest.xml";
6198
6290
  var APK_MANIFEST_PATH = "AndroidManifest.xml";
6199
6291
  var SO_HEADER_BYTES = 4096;
6200
6292
  var SO_PATH_RE = /(?:^|\/)lib\/[^/]+\/[^/]+\.so$/;
6293
+ var MODULE_MANIFEST_RE = /^([^/]+)\/manifest\/AndroidManifest\.xml$/;
6294
+ var MAX_MODULE_MANIFEST_BYTES = 1024 * 1024;
6295
+ var MAX_MODULE_MANIFESTS = 256;
6201
6296
  var SCAN_TIMEOUT_MS = 6e4;
6202
6297
  function detectManifestPath(filePath) {
6203
6298
  return filePath.toLowerCase().endsWith(".apk") ? APK_MANIFEST_PATH : AAB_MANIFEST_PATH;
@@ -6211,7 +6306,10 @@ async function readAab(aabPath) {
6211
6306
  const msg = err instanceof Error ? err.message : String(err);
6212
6307
  throw new Error(`Could not read ${aabPath}: ${msg}`, { cause: err });
6213
6308
  }
6214
- const { zipfile, entries, manifestBuf, soHeaders } = await openAndScan(buffer, manifestPath);
6309
+ const { zipfile, entries, manifestBuf, soHeaders, moduleManifests } = await openAndScan(
6310
+ buffer,
6311
+ manifestPath
6312
+ );
6215
6313
  zipfile.close();
6216
6314
  if (!manifestBuf) {
6217
6315
  const fileType = aabPath.toLowerCase().endsWith(".apk") ? "APK" : "AAB";
@@ -6227,7 +6325,20 @@ async function readAab(aabPath) {
6227
6325
  manifest = createFallbackManifest();
6228
6326
  manifest._parseError = `Manifest could not be fully parsed: ${errMsg}. Manifest-dependent checks will be skipped.`;
6229
6327
  }
6230
- return { manifest, entries, nativeLibHeaders: soHeaders };
6328
+ const deferredModules = [...moduleManifests].filter(([, buf]) => {
6329
+ try {
6330
+ return !isInstallTimeModule(buf);
6331
+ } catch {
6332
+ return false;
6333
+ }
6334
+ }).map(([name]) => name);
6335
+ return {
6336
+ manifest,
6337
+ entries,
6338
+ nativeLibHeaders: soHeaders,
6339
+ isAppBundle: manifestPath === AAB_MANIFEST_PATH,
6340
+ deferredModules
6341
+ };
6231
6342
  }
6232
6343
  function createFallbackManifest() {
6233
6344
  return {
@@ -6277,6 +6388,8 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
6277
6388
  }
6278
6389
  const entries = [];
6279
6390
  const soHeaders = /* @__PURE__ */ new Map();
6391
+ const moduleManifests = /* @__PURE__ */ new Map();
6392
+ let moduleManifestReads = 0;
6280
6393
  let manifestBuf = null;
6281
6394
  let pendingStreams = 0;
6282
6395
  let entrysDone = false;
@@ -6290,19 +6403,29 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
6290
6403
  }
6291
6404
  function maybeResolve() {
6292
6405
  if (!settled && entrysDone && pendingStreams === 0) {
6293
- settleResolve({ zipfile, entries, manifestBuf, soHeaders });
6406
+ settleResolve({ zipfile, entries, manifestBuf, soHeaders, moduleManifests });
6294
6407
  }
6295
6408
  }
6296
- function readEntryStream(entry, onData, maxBytes) {
6409
+ function readEntryStream(entry, onData, maxBytes, optional = false) {
6297
6410
  pendingStreams++;
6411
+ let done = false;
6412
+ const onError = (e) => {
6413
+ if (!optional) {
6414
+ fail(e);
6415
+ return;
6416
+ }
6417
+ if (done) return;
6418
+ done = true;
6419
+ pendingStreams--;
6420
+ maybeResolve();
6421
+ };
6298
6422
  zipfile.openReadStream(entry, (streamErr, stream) => {
6299
6423
  if (streamErr || !stream) {
6300
- fail(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
6424
+ onError(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
6301
6425
  return;
6302
6426
  }
6303
6427
  const chunks = [];
6304
6428
  let totalBytes = 0;
6305
- let done = false;
6306
6429
  stream.on("data", (chunk) => {
6307
6430
  chunks.push(chunk);
6308
6431
  totalBytes += chunk.length;
@@ -6312,7 +6435,7 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
6312
6435
  });
6313
6436
  stream.on("error", (e) => {
6314
6437
  if (maxBytes && totalBytes >= maxBytes) return;
6315
- fail(e);
6438
+ onError(e);
6316
6439
  });
6317
6440
  const finish = () => {
6318
6441
  if (done) return;
@@ -6340,10 +6463,22 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
6340
6463
  uncompressedSize: entry.uncompressedSize
6341
6464
  });
6342
6465
  }
6466
+ const moduleName = manifestPath === AAB_MANIFEST_PATH ? MODULE_MANIFEST_RE.exec(path)?.[1] : void 0;
6467
+ const readModuleManifest = moduleName !== void 0 && entry.uncompressedSize <= MAX_MODULE_MANIFEST_BYTES && moduleManifestReads < MAX_MODULE_MANIFESTS;
6343
6468
  if (path === manifestPath) {
6344
6469
  readEntryStream(entry, (buf) => {
6345
6470
  manifestBuf = buf;
6346
6471
  });
6472
+ } else if (moduleName !== void 0 && readModuleManifest) {
6473
+ moduleManifestReads++;
6474
+ readEntryStream(
6475
+ entry,
6476
+ (buf) => {
6477
+ moduleManifests.set(moduleName, buf);
6478
+ },
6479
+ MAX_MODULE_MANIFEST_BYTES,
6480
+ true
6481
+ );
6347
6482
  } else if (SO_PATH_RE.test(path)) {
6348
6483
  readEntryStream(
6349
6484
  entry,
@@ -6457,20 +6592,18 @@ var manifestScanner = {
6457
6592
  }
6458
6593
  if (manifest.targetSdk >= 34) {
6459
6594
  const hasFgsPerm = manifest.permissions.includes("android.permission.FOREGROUND_SERVICE");
6460
- if (hasFgsPerm) {
6461
- for (const service of manifest.services) {
6462
- if (!service.foregroundServiceType) {
6463
- findings.push({
6464
- scanner: "manifest",
6465
- ruleId: "foreground-service-type-missing",
6466
- severity: "error",
6467
- title: `Missing foregroundServiceType on ${service.name}`,
6468
- message: `Service "${service.name}" does not declare android:foregroundServiceType. This is required for apps targeting API 34+.`,
6469
- suggestion: `Add android:foregroundServiceType to the <service> declaration. Valid types: camera, connectedDevice, dataSync, health, location, mediaPlayback, mediaProcessing, mediaProjection, microphone, phoneCall, remoteMessaging, shortService, specialUse, systemExempted.`,
6470
- policyUrl: "https://developer.android.com/about/versions/14/changes/fgs-types-required"
6471
- });
6472
- }
6473
- }
6595
+ const anyTyped = manifest.services.some((s) => s.foregroundServiceType);
6596
+ if (hasFgsPerm && !anyTyped && manifest.services.length > 0) {
6597
+ const typedPerms = manifest.permissions.filter((p) => p.startsWith("android.permission.FOREGROUND_SERVICE_")).map((p) => p.replace("android.permission.", ""));
6598
+ findings.push({
6599
+ scanner: "manifest",
6600
+ ruleId: "foreground-service-type-missing",
6601
+ severity: "warning",
6602
+ title: "No service declares a foregroundServiceType",
6603
+ message: `The app requests FOREGROUND_SERVICE${typedPerms.length > 0 ? ` (and ${typedPerms.join(", ")})` : ""} but none of its ${manifest.services.length} services declares android:foregroundServiceType. On API 34+, any service that calls startForeground() must declare a type or it crashes at runtime. Services that never run in the foreground do not need one.`,
6604
+ suggestion: 'Add android:foregroundServiceType to each service you start in the foreground (for example dataSync, mediaPlayback, location). If none do, remove the FOREGROUND_SERVICE permission or add this rule to "disabledRules".',
6605
+ policyUrl: "https://developer.android.com/about/versions/14/changes/fgs-types-required"
6606
+ });
6474
6607
  }
6475
6608
  }
6476
6609
  const hasBackgroundLocation = manifest.permissions.includes(
@@ -6788,9 +6921,36 @@ var permissionsScanner = {
6788
6921
  }
6789
6922
  };
6790
6923
 
6924
+ // src/preflight/bundle-layout.ts
6925
+ var LIB_PATH_RE = /^(?:[^/]+\/)?lib\/([^/]+)\/[^/]+\.so$/;
6926
+ function isBundleMetadata(path) {
6927
+ return path.startsWith("BUNDLE-METADATA/") || path.startsWith("META-INF/") || path === "BundleConfig.pb";
6928
+ }
6929
+ function nativeLibAbi(path) {
6930
+ return LIB_PATH_RE.exec(path)?.[1];
6931
+ }
6932
+ function firstInstallEntries(entries, isAppBundle, deferredModules = []) {
6933
+ if (!isAppBundle) return [...entries];
6934
+ const deferred = new Set(deferredModules);
6935
+ return entries.filter(
6936
+ (e) => !isBundleMetadata(e.path) && !deferred.has(e.path.split("/", 1)[0] ?? "")
6937
+ );
6938
+ }
6939
+ function largestAbi(entries, size) {
6940
+ const perAbi = /* @__PURE__ */ new Map();
6941
+ for (const entry of entries) {
6942
+ const abi = nativeLibAbi(entry.path);
6943
+ if (abi) perAbi.set(abi, (perAbi.get(abi) ?? 0) + size(entry));
6944
+ }
6945
+ let largest;
6946
+ for (const [abi, bytes] of perAbi) {
6947
+ if (!largest || bytes > largest.bytes) largest = { abi, bytes };
6948
+ }
6949
+ return largest;
6950
+ }
6951
+
6791
6952
  // src/preflight/scanners/native-libs-scanner.ts
6792
6953
  var KNOWN_ABIS = ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"];
6793
- var LIB_PATH_RE = /^(?:[^/]+\/)?lib\/([^/]+)\/[^/]+\.so$/;
6794
6954
  var ELF_MAGIC = 2135247942;
6795
6955
  var PT_LOAD = 1;
6796
6956
  var PAGE_16KB = 16384;
@@ -6848,7 +7008,7 @@ var nativeLibsScanner = {
6848
7008
  const abisFound = /* @__PURE__ */ new Set();
6849
7009
  let totalNativeSize = 0;
6850
7010
  for (const entry of entries) {
6851
- const abi = LIB_PATH_RE.exec(entry.path)?.[1];
7011
+ const abi = nativeLibAbi(entry.path);
6852
7012
  if (abi) {
6853
7013
  abisFound.add(abi);
6854
7014
  totalNativeSize += entry.uncompressedSize;
@@ -6897,14 +7057,21 @@ var nativeLibsScanner = {
6897
7057
  title: `Native libraries: ${abiList}`,
6898
7058
  message: `Found native libraries for ${abisFound.size} architecture(s): ${abiList}. Total uncompressed size: ${sizeMb} MB.`
6899
7059
  });
6900
- if (totalNativeSize > 150 * 1024 * 1024) {
7060
+ const bundle = ctx.isAppBundle ?? false;
7061
+ const perDevice = bundle ? largestAbi(
7062
+ firstInstallEntries(entries, true, ctx.deferredModules),
7063
+ (e) => e.uncompressedSize
7064
+ ) : void 0;
7065
+ const perDeviceSize = bundle ? perDevice?.bytes ?? 0 : totalNativeSize;
7066
+ if (perDeviceSize > 150 * 1024 * 1024) {
7067
+ const perDeviceMb = (perDeviceSize / (1024 * 1024)).toFixed(1);
6901
7068
  findings.push({
6902
7069
  scanner: "native-libs",
6903
7070
  ruleId: "native-libs-large",
6904
7071
  severity: "warning",
6905
7072
  title: "Large native libraries",
6906
- message: `Native libraries total ${sizeMb} MB (uncompressed). This significantly increases download size.`,
6907
- suggestion: "Consider using Android App Bundles to deliver only the required ABI per device. Review if all native libraries are necessary."
7073
+ message: perDevice ? `Native libraries for ${perDevice.abi} total ${perDeviceMb} MB (uncompressed). Each device on that ABI downloads all of them.` : `Native libraries total ${sizeMb} MB (uncompressed). This significantly increases download size.`,
7074
+ suggestion: perDevice ? "Review whether all native libraries are necessary, and move optional native code into on-demand feature modules." : "Consider using Android App Bundles to deliver only the required ABI per device. Review if all native libraries are necessary."
6908
7075
  });
6909
7076
  }
6910
7077
  return findings;
@@ -7559,6 +7726,9 @@ var policyScanner = {
7559
7726
  };
7560
7727
 
7561
7728
  // src/preflight/scanners/size-scanner.ts
7729
+ var MB = 1024 * 1024;
7730
+ var PLAY_MOBILE_DATA_DIALOG_MB = 200;
7731
+ var mb = (bytes) => (bytes / MB).toFixed(1);
7562
7732
  var sizeScanner = {
7563
7733
  name: "size",
7564
7734
  description: "Analyzes app bundle size and warns on large downloads",
@@ -7572,21 +7742,28 @@ var sizeScanner = {
7572
7742
  const maxMb = ctx.config.maxDownloadSizeMb;
7573
7743
  const totalCompressed = entries.reduce((sum, e) => sum + e.compressedSize, 0);
7574
7744
  const totalUncompressed = entries.reduce((sum, e) => sum + e.uncompressedSize, 0);
7575
- const compressedMb = totalCompressed / (1024 * 1024);
7576
- const uncompressedMb = totalUncompressed / (1024 * 1024);
7577
- if (compressedMb > maxMb) {
7745
+ const bundle = ctx.isAppBundle ?? false;
7746
+ const installed = firstInstallEntries(entries, bundle, ctx.deferredModules);
7747
+ const nativeAbi = bundle ? largestAbi(installed, (e) => e.compressedSize) : void 0;
7748
+ const delivered = bundle ? installed.filter((e) => {
7749
+ const abi = nativeLibAbi(e.path);
7750
+ return !abi || abi === nativeAbi?.abi;
7751
+ }) : installed;
7752
+ const downloadBytes = delivered.reduce((sum, e) => sum + e.compressedSize, 0);
7753
+ if (downloadBytes / MB > maxMb) {
7754
+ const size = bundle ? `Estimated download for one device is ${mb(downloadBytes)} MB${nativeAbi ? ` (with ${nativeAbi.abi} native libraries)` : ""}` : `The APK is ${mb(downloadBytes)} MB compressed`;
7578
7755
  findings.push({
7579
7756
  scanner: "size",
7580
7757
  ruleId: "size-over-limit",
7581
7758
  severity: "warning",
7582
7759
  title: `Download size exceeds ${maxMb} MB`,
7583
- message: `Compressed size is ${compressedMb.toFixed(1)} MB. Downloads over ${maxMb} MB show a mobile data warning to users, which reduces install rates.`,
7584
- suggestion: "Use Android App Bundles for split APKs, remove unused resources, enable R8/ProGuard, and compress assets.",
7585
- policyUrl: "https://developer.android.com/topic/performance/reduce-apk-size"
7760
+ message: `${size}, above your ${maxMb} MB threshold.` + (downloadBytes > PLAY_MOBILE_DATA_DIALOG_MB * MB ? ` Google Play shows users on mobile data a large-download dialog for apps over ${PLAY_MOBILE_DATA_DIALOG_MB} MB, which can reduce install rates.` : ""),
7761
+ suggestion: bundle ? "Remove unused resources, enable R8, and move large or optional content into fast-follow or on-demand asset packs or feature modules." : "Publish an Android App Bundle so each device downloads only its own ABI and screen density, remove unused resources, and enable R8.",
7762
+ policyUrl: "https://support.google.com/googleplay/android-developer/answer/9859372"
7586
7763
  });
7587
7764
  }
7588
7765
  const categories = /* @__PURE__ */ new Map();
7589
- for (const entry of entries) {
7766
+ for (const entry of delivered) {
7590
7767
  const cat = detectCategory(entry.path);
7591
7768
  const existing = categories.get(cat) ?? { compressed: 0, uncompressed: 0, count: 0 };
7592
7769
  existing.compressed += entry.compressedSize;
@@ -7595,13 +7772,13 @@ var sizeScanner = {
7595
7772
  categories.set(cat, existing);
7596
7773
  }
7597
7774
  const nativeLibs = categories.get("native-libs");
7598
- if (nativeLibs && nativeLibs.compressed > 50 * 1024 * 1024) {
7775
+ if (nativeLibs && nativeLibs.compressed > 50 * MB) {
7599
7776
  findings.push({
7600
7777
  scanner: "size",
7601
7778
  ruleId: "size-large-native",
7602
7779
  severity: "warning",
7603
7780
  title: "Large native libraries",
7604
- message: `Native libraries are ${(nativeLibs.compressed / (1024 * 1024)).toFixed(1)} MB (compressed). This is the largest contributor to download size.`,
7781
+ message: nativeAbi ? `Native libraries for ${nativeAbi.abi} are ${mb(nativeLibs.compressed)} MB (compressed), and each device on that ABI downloads all of them.` : `Native libraries are ${mb(nativeLibs.compressed)} MB (compressed) across every ABI in the APK.`,
7605
7782
  suggestion: "Review which native libraries are bundled. Consider using dynamic feature modules for optional native code."
7606
7783
  });
7607
7784
  }
@@ -7612,18 +7789,18 @@ var sizeScanner = {
7612
7789
  ruleId: "size-large-assets",
7613
7790
  severity: "info",
7614
7791
  title: "Large assets directory",
7615
- message: `Assets are ${(assets.compressed / (1024 * 1024)).toFixed(1)} MB (compressed). Consider using Play Asset Delivery for large assets.`,
7792
+ message: `Assets are ${mb(assets.compressed)} MB (compressed). Consider using Play Asset Delivery for large assets.`,
7616
7793
  suggestion: "Move large assets to Play Asset Delivery (install-time, fast-follow, or on-demand packs).",
7617
7794
  policyUrl: "https://developer.android.com/guide/playcore/asset-delivery"
7618
7795
  });
7619
7796
  }
7620
- const breakdown = [...categories.entries()].sort((a, b) => b[1].compressed - a[1].compressed).map(([cat, data]) => `${cat}: ${(data.compressed / (1024 * 1024)).toFixed(1)} MB`).join(", ");
7797
+ const breakdown = [...categories.entries()].sort((a, b) => b[1].compressed - a[1].compressed).map(([cat, data]) => `${cat}: ${mb(data.compressed)} MB`).join(", ");
7621
7798
  findings.push({
7622
7799
  scanner: "size",
7623
7800
  ruleId: "size-summary",
7624
7801
  severity: "info",
7625
- title: `Total size: ${compressedMb.toFixed(1)} MB compressed, ${uncompressedMb.toFixed(1)} MB uncompressed`,
7626
- message: `${entries.length} files. Breakdown: ${breakdown}`
7802
+ title: bundle ? `Estimated download: ${mb(downloadBytes)} MB per device (bundle file ${mb(totalCompressed)} MB compressed, ${mb(totalUncompressed)} MB uncompressed)` : `Total size: ${mb(totalCompressed)} MB compressed, ${mb(totalUncompressed)} MB uncompressed`,
7803
+ message: bundle ? `${delivered.length} of ${entries.length} files reach one device. Breakdown: ${breakdown}` : `${entries.length} files. Breakdown: ${breakdown}`
7627
7804
  });
7628
7805
  return findings;
7629
7806
  }
@@ -7631,10 +7808,10 @@ var sizeScanner = {
7631
7808
  function detectCategory(path) {
7632
7809
  const lower = path.toLowerCase();
7633
7810
  if (lower.endsWith(".dex") || /\/dex\//.test(lower)) return "dex";
7634
- if (/\/lib\/[^/]+\/[^/]+\.so$/.test(lower)) return "native-libs";
7635
- if (/\/res\//.test(lower) || lower.endsWith("/resources.pb") || lower.endsWith("/resources.arsc"))
7811
+ if (nativeLibAbi(path)) return "native-libs";
7812
+ if (/(?:^|\/)res\//.test(lower) || /(?:^|\/)resources\.(?:pb|arsc)$/.test(lower))
7636
7813
  return "resources";
7637
- if (/\/assets\//.test(lower)) return "assets";
7814
+ if (/(?:^|\/)assets\//.test(lower)) return "assets";
7638
7815
  if (lower.includes("androidmanifest.xml") || /\/manifest\//.test(lower)) return "manifest";
7639
7816
  if (lower.startsWith("meta-inf/")) return "signing";
7640
7817
  return "other";
@@ -7670,6 +7847,8 @@ async function runPreflight(options) {
7670
7847
  ctx.manifest = aab.manifest;
7671
7848
  ctx.zipEntries = aab.entries;
7672
7849
  ctx.nativeLibHeaders = aab.nativeLibHeaders;
7850
+ ctx.isAppBundle = aab.isAppBundle;
7851
+ ctx.deferredModules = aab.deferredModules;
7673
7852
  if (aab.manifest._parseError) {
7674
7853
  earlyFindings.push({
7675
7854
  scanner: "manifest-parser",
@@ -10151,6 +10330,7 @@ export {
10151
10330
  batchGetOrders,
10152
10331
  batchSyncInAppProducts,
10153
10332
  buildChecklist,
10333
+ buildExternalTransactionRefund,
10154
10334
  buildLocaleBundle,
10155
10335
  bundleToReleaseNotes,
10156
10336
  cancelRecoveryAction,